﻿namespace EyeOpen.Mapping.Nhibernate
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Xml.Linq;

    public abstract class NhibernateMapper : BaseMapper
    {
        private bool notNullDefault = true;
        private string generatorTypeName;
        private Type idType = typeof(Guid);

        public NhibernateMapper() 
        {
        }

        public NhibernateMapper(string generatorTypeName)
        {
            this.SetGeneratorTypeName(generatorTypeName);
        }

        public bool NotNullDefault
        {
            get { return this.notNullDefault; }
            set { this.notNullDefault = value; }
        }

        public string GeneratorTypeName
        {
            get { return this.generatorTypeName; }
            set { this.SetGeneratorTypeName(value); }
        }

        public Type IdType
        {
            get 
            { 
                return this.idType; 
            }

            set 
            {
                if ((this.idType != typeof(Guid)) &&
                    (this.idType != typeof(int)) &&
                    (this.idType != typeof(long)))
                {
                    throw new ArgumentException("Valid types are only System.Int32 and System.Guid.");
                }

                this.idType = value;            
            }
        }

        public override IDictionary<Type, XElement> GenerateMappings(Assembly assembly)
        {
            return base.GenerateMappings(assembly, type => !type.IsAbstract);
        }

        public override XElement GenerateMappings(Type type)
        {
            XNamespace ns = XNamespace.Get("urn:nhibernate-mapping-2.2");
            XElement element = new XElement(ns + "hibernate-mapping");
            string assemblyName = type.Assembly.GetName().Name;

            element.SetAttributeValue("namespace", assemblyName);
            element.SetAttributeValue("assembly", assemblyName);
            element.SetAttributeValue("default-access", "field.camelcase-underscore");
            element.SetAttributeValue("default-cascade", "all");
            element.SetAttributeValue("default-lazy", "true");
            XElement classElement = new XElement(ns + "class");
            classElement.SetAttributeValue("name", type.Name);
            classElement.SetAttributeValue("table", type.Name);
            element.Add(classElement);
            classElement.Add(this.AddId(ns));

            PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);

            PropertyComparer propertyComparer = new PropertyComparer();

            properties
            .Where(property => this.CanMapProperty(property))
            .ToList()
            .OrderBy<PropertyInfo, PropertyInfo>(property => property, propertyComparer)
            .ToList()
            .ForEach(property => this.AddProperty(ns, classElement, property));            

            return element;
        }

        protected virtual bool CanMapProperty(PropertyInfo property)
        {
            return true;
        }

        protected virtual void AddProperty(XNamespace ns, XElement classElement, PropertyInfo propertyInfo)
        {
            if (IsBaseType(propertyInfo.PropertyType))
            {
                this.MapBaseType(ns, classElement, propertyInfo);
            }
            else
            {
                this.MapComplexType(ns, classElement, propertyInfo);
            }
        }

        protected virtual XElement AddId(XNamespace ns)
        {
            XElement idElement = new XElement(ns + "property");
            idElement = new XElement(ns + "id");
            idElement.SetAttributeValue("name", "Id");
            idElement.SetAttributeValue("type", this.IdType.FullName);
            idElement.SetAttributeValue("unsaved-value", Activator.CreateInstance(IdType).ToString());
            if (!string.IsNullOrEmpty(this.GeneratorTypeName))
            {
                XElement generator = new XElement(ns + "generator");
                generator.SetAttributeValue("class", this.GeneratorTypeName);
                idElement.Add(generator);
            }

            return idElement;
        }

        protected virtual void MapBaseType(XNamespace ns, XElement classElement, PropertyInfo propertyInfo)
        {
            XElement propertyElement = new XElement(ns + "property");
            propertyElement.SetAttributeValue("name", propertyInfo.Name);
            propertyElement.SetAttributeValue("type", propertyInfo.PropertyType.FullName);
            XElement columnElement = new XElement(ns + "column");
            columnElement.SetAttributeValue("name", propertyInfo.Name);
            columnElement.SetAttributeValue("not-null", this.NotNullDefault);
            propertyElement.Add(columnElement);
            classElement.Add(propertyElement);
        }

        protected virtual void MapComplexType(XNamespace ns, XElement classElement, PropertyInfo propertyInfo)
        {
            XElement complexElement = new XElement(ns + "many-to-one");
            complexElement.SetAttributeValue("name", propertyInfo.Name);
            XElement columnElement = new XElement(ns + "column");
            columnElement.SetAttributeValue("name", string.Format("{0}Id", propertyInfo.Name));
            columnElement.SetAttributeValue("not-null", this.NotNullDefault);
            complexElement.Add(columnElement);
            classElement.Add(complexElement);
        }

        private void SetGeneratorTypeName(string generatorTypeName)
        {
            if (string.IsNullOrEmpty(generatorTypeName.Trim()))
            {
                throw new ArgumentNullException();
            }

            this.generatorTypeName = generatorTypeName;
        }
    }
}